/*
 * Copyright (c) 2006 - 2022, webrx.cn All rights reserved.
 *
 */
package cn.webrx;

import com.google.typography.font.sfntly.Font;
import com.google.typography.font.sfntly.FontFactory;
import com.google.typography.font.sfntly.Tag;
import com.google.typography.font.sfntly.table.Table;
import com.google.typography.font.sfntly.table.core.CMap;
import com.google.typography.font.sfntly.table.core.CMapFormat12;
import com.google.typography.font.sfntly.table.core.CMapTable;
import com.google.typography.font.sfntly.table.core.PostScriptTable;

import java.io.*;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.zip.DataFormatException;
import java.util.zip.Inflater;

/**
 * <p>Project: app2022 - WoffConverter
 * <p>Powered by webrx On 2022-01-27 13:47:37
 *
 * @author webrx [webrx@126.com]
 * @version 1.0
 * @since 17
 */
public class WoffConverter {

    private static final LinkedHashMap<String, Integer> woffHeaderFormat = new LinkedHashMap<String, Integer>() {
        {
            put("signature", 4);
            put("flavor", 4);
            put("length", 4);
            put("numTables", 2);
            put("reserved", 2);
            put("totalSfntSize", 4);
            put("majorVersion", 2);
            put("minorVersion", 2);
            put("metaOffset", 4);
            put("metaLength", 4);
            put("metaOrigLength", 4);
            put("privOffset", 4);
            put("privOrigLength", 4);
        }
    };

    //private static Logger logger = LoggerFactory.getLogger(WoffConverter.class);
    private static final LinkedHashMap<String, Integer> tableRecordEntryFormat = new LinkedHashMap<String, Integer>() {
        {
            put("tag", 4);
            put("offset", 4);
            put("compLength", 4);
            put("origLength", 4);
            put("origChecksum", 4);
        }
    };
    private final HashMap<String, Number> woffHeaders = new HashMap<String, Number>();
    private final ArrayList<HashMap<String, Number>> tableRecordEntries = new ArrayList<HashMap<String, Number>>();
    private int offset = 0;
    private int readOffset = 0;
    private File woffFile;
    private byte[] ttfByteArray;

    private WoffConverter() {
    }

    public WoffConverter(File woffFile) throws Exception {
        this.woffFile = woffFile;
        FileInputStream inputStream = new FileInputStream(woffFile);
        ByteArrayOutputStream ttfOutputStream = convertToTTFOutputStream(inputStream);
        ttfByteArray = ttfOutputStream.toByteArray();

    }

    public static void main(String[] args) throws Exception {
        var wc = new WoffConverter(new File("d:\\zh34hsnhft.woff"));
        wc.writeTableRecordEntries(new ByteArrayOutputStream());
    }

    /**
     * woff 转 ttf byte[]
     *
     * @return
     * @throws InvalidWoffException
     * @throws IOException
     * @throws DataFormatException
     */
    public byte[] getTTFByteArray() {
        return ttfByteArray;
    }

    /**
     * 获取Cmap
     *
     * @return
     */
    public LinkedHashMap<Integer, String> getCmap() {
        LinkedHashMap<Integer, String> ret = new LinkedHashMap<Integer, String>();
        try {
            FontFactory fontFactory = FontFactory.getInstance();
            Font font = fontFactory.loadFonts(ttfByteArray)[0];

            Map<Integer, ? extends Table> tableMap = font.tableMap();
            CMapTable cmapTable = (CMapTable) tableMap.get(Tag.cmap);

            Iterator<CMap> it = cmapTable.iterator();

            while (it.hasNext()) {
                CMap cmap = it.next();
                if (cmap instanceof CMapFormat12) {
                    Iterator<Integer> it1 = cmap.iterator();
                    while (it1.hasNext()) {
                        int val = it1.next();
                        String unicode = val < 128 ? String.valueOf((char) val) : ("uni" + Integer.toHexString(val));

                        ret.put(val, unicode);
                    }
                    break;
                }

            }

        } catch (Exception e) {
            e.printStackTrace();
        }
        return ret;

    }


    /**
     * 获取unicode 字符列表
     *
     * @return
     */
    public List<String> getUniCodeList() {
        List<String> works = new ArrayList<String>();
        try {
            FontFactory fontFactory = FontFactory.getInstance();
            Font font = fontFactory.loadFonts(ttfByteArray)[0];

            Map<Integer, ? extends Table> tableMap = font.tableMap();
            if (tableMap.containsKey(Tag.CFF)) {

            } else if (tableMap.containsKey(Tag.post)) {
                PostScriptTable postScriptTable = (PostScriptTable) tableMap.get(Tag.post);
                for (int i = 0; i < postScriptTable.numberOfGlyphs(); i++) {
                    String glypName = postScriptTable.glyphName(i);
                    if (!glypName.startsWith("uni")) {
                        continue;
                    }
                    works.add(glypName);
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
        return works;

    }


    private ByteArrayOutputStream convertToTTFOutputStream(InputStream inputStream)
            throws Exception {
        getHeaders(new DataInputStream(inputStream));
        if ((Integer) woffHeaders.get("signature") != 0x774F4646) {
            throw new Exception("Invalid woff file");
        }
        ByteArrayOutputStream ttfOutputStream = new ByteArrayOutputStream();
        writeOffsetTable(ttfOutputStream);
        getTableRecordEntries(new DataInputStream(inputStream));
        writeTableRecordEntries(ttfOutputStream);
        writeFontData(inputStream, ttfOutputStream);
        return ttfOutputStream;
    }

    /**
     * 获取头部
     *
     * @param woffFileStream
     * @throws IOException
     */
    private void getHeaders(DataInputStream woffFileStream) throws IOException {
        readTableData(woffFileStream, woffHeaderFormat, woffHeaders);
    }

    /**
     * @param ttfOutputStream
     * @throws IOException
     */
    private void writeOffsetTable(ByteArrayOutputStream ttfOutputStream)
            throws IOException {
        ttfOutputStream.write(getBytes((Integer) woffHeaders.get("flavor")));
        int numTables = (Integer) woffHeaders.get("numTables");
        ttfOutputStream.write(getBytes((short) numTables));
        int temp = numTables;
        int searchRange = 16;
        short entrySelector = 0;
        while (temp > 1) {
            temp = temp >> 1;
            entrySelector++;
            searchRange = (searchRange << 1);
        }
        short rangeShift = (short) (numTables * 16 - searchRange);
        ttfOutputStream.write(getBytes((short) searchRange));
        ttfOutputStream.write(getBytes(entrySelector));
        ttfOutputStream.write(getBytes(rangeShift));
        offset += 12;
    }

    private void getTableRecordEntries(DataInputStream woffFileStream)
            throws IOException {
        int numTables = (Integer) woffHeaders.get("numTables");
        for (int i = 0; i < numTables; i++) {
            HashMap<String, Number> tableDirectory = new HashMap<String, Number>();
            readTableData(woffFileStream, tableRecordEntryFormat,
                    tableDirectory);
            offset += 16;
            tableRecordEntries.add(tableDirectory);
        }
    }

    private void writeTableRecordEntries(ByteArrayOutputStream ttfOutputStream)
            throws IOException {
        for (HashMap<String, Number> tableRecordEntry : tableRecordEntries) {
            ttfOutputStream.write(getBytes((Integer) tableRecordEntry
                    .get("tag")));
            ttfOutputStream.write(getBytes((Integer) tableRecordEntry
                    .get("origChecksum")));
            ttfOutputStream.write(getBytes(offset));
            ttfOutputStream.write(getBytes((Integer) tableRecordEntry
                    .get("origLength")));
            tableRecordEntry.put("outOffset", offset);
            offset += (Integer) tableRecordEntry.get("origLength");
            if (offset % 4 != 0) {
                offset += 4 - (offset % 4);
            }
        }
    }

    private void writeFontData(InputStream woffFileStream, ByteArrayOutputStream ttfOutputStream) throws IOException,
            DataFormatException {
        for (HashMap<String, Number> tableRecordEntry : tableRecordEntries) {
            int tableRecordEntryOffset = (Integer) tableRecordEntry
                    .get("offset");
            int skipBytes = tableRecordEntryOffset - readOffset;
            if (skipBytes > 0) {
                woffFileStream.skip(skipBytes);
            }
            readOffset += skipBytes;
            int compressedLength = (Integer) tableRecordEntry.get("compLength");
            int origLength = (Integer) tableRecordEntry.get("origLength");
            byte[] fontData = new byte[compressedLength];
            byte[] inflatedFontData = new byte[origLength];
            int readBytes = 0;
            while (readBytes < compressedLength) {
                readBytes += woffFileStream.read(fontData, readBytes,
                        compressedLength - readBytes);
            }
            readOffset += compressedLength;
            inflatedFontData = inflateFontData(compressedLength,
                    origLength, fontData, inflatedFontData);
            ttfOutputStream.write(inflatedFontData);
            offset = (Integer) tableRecordEntry.get("outOffset")
                    + (Integer) tableRecordEntry.get("origLength");
            int padding = 0;
            if (offset % 4 != 0) {
                padding = 4 - (offset % 4);
            }
            ttfOutputStream.write(getBytes(0), 0, padding);
        }
    }

    private byte[] inflateFontData(int compressedLength, int origLength,
                                   byte[] fontData, byte[] inflatedFontData) {
        if (compressedLength != origLength) {
            Inflater decompressor = new Inflater();
            decompressor.setInput(fontData, 0, compressedLength);
            try {
                decompressor.inflate(inflatedFontData, 0, origLength);
            } catch (Exception e) {
                e.printStackTrace();
            }
        } else {
            inflatedFontData = fontData;
        }
        return inflatedFontData;
    }

    private byte[] getBytes(int i) {
        return ByteBuffer.allocate(4).putInt(i).array();
    }

    private byte[] getBytes(short h) {
        return ByteBuffer.allocate(2).putShort(h).array();
    }

    private void readTableData(DataInputStream woffFileStream,
                               LinkedHashMap<String, Integer> formatTable,
                               HashMap<String, Number> table) throws IOException {
        Iterator<String> headerKeys = formatTable.keySet().iterator();
        while (headerKeys.hasNext()) {
            String key = headerKeys.next();
            int size = formatTable.get(key);
            if (size == 2) {
                table.put(key, woffFileStream.readUnsignedShort());
            } else if (size == 4) {
                table.put(key, woffFileStream.readInt());
            }
            readOffset += size;
        }
    }
}
